/*
 * Copyright (c) 2012 RTCLab
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this list of
 *    conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list
 *    of conditions and the following disclaimer in the documentation and/or other materials
 *    provided with the distribution.
 *
 */

package com.rtckit {

import flash.media.*;
import flash.events.*;
import flash.net.*;
import flash.utils.Timer;
import com.adobe.serialization.json.Json;
import flash.system.Security;
import flash.system.SecurityPanel;

public class Softphone {
	private var nc:NetConnection = null;
	private var reconnectTimer:Timer = null;
	private var reconnectDelay:Number = 3000;
	
	private var pingTimer:Timer = null;
	private var pingTimeout:Number = 10000;
	private var pings:Number = 0;

	private var resp:Responder;

	private var outMedia:OutgoingMedia;
	private var inMedia:IncomingMedia;

	private var rings:RingManager = null;

	private var config:SoftphoneConfig;
	
	private var autoReg:Object = null;
	private var autoCallNumber:String = null;

	private var callState:String = 'IDLE';
	private var lastDialed:String = '';

	function Softphone(config:SoftphoneConfig)
	{
		this.config = config;
		
		resp = new Responder(onComplete, onFail);
		
		if (!config.stresstest) {
			rings = new RingManager();
			rings.addSound('ringback', config.ringback);
			rings.addSound('ring', config.ring);
			rings.addSound('busy', config.busy, 1);
			rings.addSound('noanswer', config.noanswer, 1);
		}
		
		reconnectTimer = new Timer(reconnectDelay, 1);
		reconnectTimer.addEventListener(flash.events.TimerEvent.TIMER, onReconnectTimer);
		
		pingTimer = new Timer(pingTimeout);
		pingTimer.addEventListener(flash.events.TimerEvent.TIMER, onPingTimer);
	}
	
	// needed for stress test
	
	public function setAutoReg(reg:Object):void {
		autoReg = reg;
	}
	
	public function setAutoCall(number:String):void {
		autoCallNumber = number;
	}
	
	// json
	
	private function toJSON(obj:Object):String {
		return Json.encode(obj);
	}
	
	private function fromJSON(json:String):Object {
		return Json.decode(json);
	}
	
	// connection
	
	private function connectImpl(uri:String):void {
		nc = new NetConnection();
		nc.client = this;
		nc.addEventListener(NetStatusEvent.NET_STATUS, ncStatus);
		nc.connect(uri);
	}
	
	public function connect(uri:String):void {
		if (nc)
			disconnect();
		connectImpl(uri);
	}
	
	public function reconnect():void {
		if (!nc)
			return;
		connectImpl(nc.uri);
	}
	
	public function disconnect():void {
		nc.close();
		nc = null;
	}
	
	public function ping():void {
		if (nc)
			nc.call("ping", resp);
	}
	
	private function ncStatus(event:NetStatusEvent):void {
		Logger.debug('Connection: ' + event.info.code);

		switch (event.info.code) {
		case "NetConnection.Connect.Success":
			reconnectTimer.reset();
			inMedia = new IncomingMedia(nc, config);
			outMedia = new OutgoingMedia(nc, config);
			config.reportConnectState(true);
			if (autoReg) {
				register(autoReg);
			}
			pings = 0;
			pingTimer.reset();
			pingTimer.start();
			break;
		case "NetConnection.Ping.Success":
			pings = 0;
			break;
		case "NetConnection.Connect.Closed":
		case "NetConnection.Connect.Failed":
			if (callState != 'IDLE')
				onCallStateImpl({state: 'IDLE', uri: lastDialed});
			inMedia.destroy();
			outMedia.destroy();
			onRegState(toJSON({registered: false, info: 'Connection closed'}));
			config.reportConnectState(false);
			reconnectTimer.start();
			pingTimer.stop();
			break;
		case "NetStream.Connect.Success":
			var peerID:String = event.info.stream.farID;
			Logger.debug('P2P connected: ' + peerID);
			inMedia.peerConnected(peerID);
			outMedia.peerConnected(peerID);
			break;
		}
	}
	
	private function onReconnectTimer(event:TimerEvent):void {
		if (!nc)
			return;
		Logger.info('Reconnecting...');
		reconnect();
	}
	
	private function onPingTimer(event:TimerEvent):void {
		if (!nc || !nc.connected)
			return;
		pings++;
		ping();
		if (pings > 1) {
			Logger.info('Ping failed: disconnecting and trying to reconnect...');
			var uri:String = nc.uri;
			disconnect();
			reconnectTimer.reset();
			connect(uri);
		}
	}
	
	// registration
	
	public function register(args:Object):void {
		if (nc)
			nc.call("register", resp, toJSON(args));
	}

	public function unregister(args:Object):void {
		if (nc)
			nc.call("unregister", resp);
	}
	
	public function onRegState(json:String):void {
		var args:Object = fromJSON(json);
		if (args.info == '-')
			args.info = '';
		if (args.registered && autoCallNumber) {
			Logger.info('Calling: ' + autoCallNumber);
			call({uri: autoCallNumber});
			autoCallNumber = null;
		}
		config.reportRegState(args);
	}
	
	// calls
	
	public function call(args:Object):void {
		if (nc)
			nc.call("call", resp, toJSON(args));
	}

	public function hangup(args:Object):void {
		if (nc)
			nc.call("hangup", resp, toJSON(args));
	}

	public function answer(args:Object):void {
		if (nc)
			nc.call("answer", resp, toJSON(args));
	}

	public function dial(args:Object):void {
		if (nc)
			nc.call("dial", resp, toJSON(args));
		rings.dial(args.digits);
	}
	
	private var noAnswerReasons:Array = [
	    'noanswer',
	    'offline',
	    'forbidden',
	    'noroute',
	    'timeout',
	    'no route',
	    'failure',
	    'channel limit exceeded'
	];
	
	public function isNoAnswer(info:String):Boolean {
		var lc:String = info.toLowerCase();
		for (var i:Number = 0; i < noAnswerReasons.length; i++) {
			if (lc.indexOf(noAnswerReasons[i]) != -1)
				return true;
		}
		return false;
	}
	
	public function handleMedia(state:String, hasVideo:Boolean, audioCodec:String, peerID:String):void {
		switch (state) {
		case 'IDLE':
			inMedia.disconnect();
			outMedia.disconnect();
			break;
		case 'INCOMING':
		case 'OUTGOING':
			inMedia.prepare();
			break;
		case 'CONNECTED':
			inMedia.connect(hasVideo, peerID);
			outMedia.connect(nc.nearID, hasVideo, audioCodec, peerID);
			break;
		}
	}
	
	public function handleRings(state:String, info:String):void {
		if (!rings)
			return;
		rings.stopAll();
		switch (state) {
		case 'OUTGOING':
			rings.play('ringback');
			break;
		case 'INCOMING':
			rings.play('ring');
			break;
		case 'IDLE':
			if (info.toLowerCase().indexOf('busy') != -1)
				rings.play('busy');
			else if (isNoAnswer(info))
				rings.play('noanswer');
		}
	}
	
	public function onCallState(json:String):void {
		var args:Object = fromJSON(json);
		onCallStateImpl(args);
	}
	
	public function onCallStateImpl(args:Object):void {
		if (args.info == '-' || !args.info)
			args.info = '';
		
		if (args.state != 'IDLE')
			lastDialed = args.uri;

		if (args.state != callState) {
			handleMedia(args.state, args.video, args.audioCodec, args.peerID);
			handleRings(args.state, args.info);
		}

		callState = args.state;
		config.reportCallState(args);
	}
	
	public function getQualityStats():Object {
		if (callState != 'CONNECTED')
			return null;
		return {
			sendAudioLossRate: outMedia.getAudioLossRate(),
			recvAudioLossRate: inMedia.getAudioLossRate()
		};
	}

	// responder events

	private function onComplete(results:Object):void {
		Logger.debug('Remote call complete');
	}

	private function onFail(results:Object):void {
		Logger.debug('Remote call failed');
	}

}

}
